﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using System.Xml;
using System.Runtime.Serialization;

namespace ArgusPCLib.Bird
{
	public sealed class LogXmlWriter : LogWriter
	{
		const string XmlNameRootElement = "Log";
		const string XmlNameMessage = "Message";
		const string XmlNameMethodEnter = "MethodEnter";
		const string XmlNameMethodLeave = "MethodLeave";
		const string XmlNameThread = "ThreadID";
		const string XmlNameTimestamp = "Timestamp";
		const string XmlNameCallerName = "MethodName";
		const string XmlNameCallerFilePath = "CodeFilePath";
		const string XmlNameCallerLineNumber = "CodeLineNumber";
		const string XmlNameDeclaringType = "DeclaringType";
		const string XmlNameAttributes = "Attributes";
		const string XmlNameAttribute = "Attribute";
		const string XmlNameParameters = "Parameters";
		const string XmlNameParameter = "Parameter";
		const string XmlNameIsByRef = "IsByRef";
		const string XmlNameType = "Type";
		const string XmlNameValue = "Value";
		const string XmlNameReturnParameter = "ReturnParameter";
		const string XmlNameOutputParameters = "OutputParameters";
		const string XmlNameException = "Exception";
		const string XmlNameExceptionMessage = "Message";
		const string XmlNameStackTrace = "StackTrace";
		const string XmlNameExceptionData = "ExceptionData";
		const string XmlNameData = "Data";
		const string XmlNameKey = "Key";
		const string XmlNameGenericTypeParameters = "GenericTypeParameters";

		XmlWriter xmlWriter;

		public Stream BaseStream { get; private set; }

		public override bool IsDisposed { get { return this.xmlWriter == null; } }

		public LogXmlWriter(Stream stream, XmlWriterSettings xmlWriterSettings)
		{
			if (xmlWriterSettings == null)
			{
				xmlWriterSettings = new XmlWriterSettings();
				xmlWriterSettings.Indent = true;
				xmlWriterSettings.IndentChars = "  ";
				xmlWriterSettings.NewLineOnAttributes = false;
				xmlWriterSettings.WriteEndDocumentOnClose = true;
				xmlWriterSettings.NewLineChars = System.Environment.NewLine;
				xmlWriterSettings.NewLineHandling = NewLineHandling.Replace;
				xmlWriterSettings.NewLineOnAttributes = true;
			}
			this.BaseStream = stream;
			this.xmlWriter = XmlWriter.Create(stream, xmlWriterSettings);
			this.xmlWriter.WriteStartDocument();
			this.xmlWriter.WriteStartElement(XmlNameRootElement);
		}

		public LogXmlWriter(Stream stream)
			: this(stream, null) { }

		public override void Dispose()
		{
			if (this.xmlWriter != null)
			{
				lock(this.xmlWriter)
				{
					if (this.xmlWriter != null)
					{
						this.xmlWriter.WriteEndDocument();
						this.xmlWriter.Flush();
						this.xmlWriter.Dispose();
						this.xmlWriter = null;
					}
				}
			}
		}

		private void WriteAttributes(string callerName, string callerFilePath, int callerLineNumber)
		{
			this.xmlWriter.WriteAttributeString(XmlNameThread, Environment.CurrentManagedThreadId.ToStringInvariant());
			this.xmlWriter.WriteAttributeString(XmlNameTimestamp, DateTime.UtcNow.ToDateTimeStringInvariant());
			if (!string.IsNullOrEmpty(callerName))
				this.xmlWriter.WriteAttributeString(XmlNameCallerName, callerName);
			if (!string.IsNullOrEmpty(callerFilePath))
				this.xmlWriter.WriteAttributeString(XmlNameCallerFilePath, callerFilePath);
			if (callerLineNumber > 0)
				this.xmlWriter.WriteAttributeString(XmlNameCallerLineNumber, callerLineNumber.ToStringInvariant());
		}

		public override void LogMessage(string callerName, string callerFilePath, int callerLineNumber, string message)
		{
			lock (this.xmlWriter)
			{
				this.xmlWriter.WriteStartElement(XmlNameMessage);
				this.WriteAttributes(callerName, callerFilePath, callerLineNumber);
				this.xmlWriter.WriteValue(message);
				this.xmlWriter.WriteEndElement();
				this.xmlWriter.Flush();
			}
		}

		private void WriteGenericTypeParameters(MethodInfo methodInfo)
		{
			if (!methodInfo.IsGenericMethod)
				return;

			this.xmlWriter.WriteStartElement(XmlNameGenericTypeParameters);
			Type[] genericTypes = methodInfo.GetGenericMethodDefinition().GetGenericArguments();
			Type[] values = methodInfo.GetGenericArguments();
			for (int i = 0; i < values.Length; i++)
			{
				this.xmlWriter.WriteStartElement(XmlNameParameter);
					this.xmlWriter.WriteElementString(XmlNameParameter, genericTypes[i].Name);
					this.xmlWriter.WriteElementString(XmlNameValue, values[i].AssemblyQualifiedName);
				this.xmlWriter.WriteEndElement();
			}
			this.xmlWriter.WriteEndElement();
		}

		private void WriteMethodAttributes(MethodInfo methodInfo)
		{
			Array values = Enum.GetValues(typeof(MethodAttributes));
			this.xmlWriter.WriteStartElement(XmlNameAttributes);
			foreach (MethodAttributes attr in values)
			{
				if (methodInfo.Attributes.HasFlag(attr))
					this.xmlWriter.WriteElementString(XmlNameAttribute, attr.ToString());
			}
			this.xmlWriter.WriteEndElement();
		}

		public override void LogMethodEnter(string callerName, string callerFilePath, int callerLineNumber, MethodInfo methodInfo, object[] arguments)
		{
			lock (this.xmlWriter)
			{
				//Delegate methodEnter = new Action<string, string, int, Delegate, object[]>(this.LogMethodEnter);
				//if (Logger.Verbosity <= LogVerbosities.All && methodEnter != methodHandle)
				//	this.LogMethodEnter(null, null, 0, methodEnter, new object[]{ methodHandle, arguments});

				this.xmlWriter.WriteStartElement(XmlNameMethodEnter);
				this.WriteAttributes(callerName, callerFilePath, callerLineNumber);
				if (methodInfo != null)
				{
					if (arguments == null)
						arguments = new object[0];
					
					this.xmlWriter.WriteStartElement(methodInfo.Name);
						this.xmlWriter.WriteElementString(XmlNameDeclaringType, methodInfo.DeclaringType.AssemblyQualifiedName);
						this.WriteMethodAttributes(methodInfo);
						this.WriteGenericTypeParameters(methodInfo);

						ParameterInfo[] pis = methodInfo.GetParameters();
						this.xmlWriter.WriteStartElement(XmlNameParameters);
							for (int i = 0; i < pis.Length; i++)
							{
								this.xmlWriter.WriteStartElement(pis[i].Name);
									if (pis[i].ParameterType.IsByRef)
										this.xmlWriter.WriteAttributeString(XmlNameIsByRef, "True");
									this.xmlWriter.WriteElementString(XmlNameType, pis[i].ParameterType.AssemblyQualifiedName);
									if (arguments.Length > i)
										this.xmlWriter.WriteElementObject(XmlNameValue, arguments[i]);
								this.xmlWriter.WriteEndElement();
							}
						this.xmlWriter.WriteEndElement();
					this.xmlWriter.WriteEndElement();
					
				}
				this.xmlWriter.WriteEndElement();
				this.xmlWriter.Flush();

				//if (Logger.Verbosity <= LogVerbosities.All && methodEnter != methodHandle)
				//	this.LogMethodLeave(null, null, 0, methodEnter, new object[0]);
			}
		}

		public override void LogMethodLeave(string callerName, string callerFilePath, int callerLineNumber, MethodInfo methodInfo, object[] returnValues)
		{
			lock (this.xmlWriter)
			{
				//Delegate methodLeave = new Action<string, string, int, Delegate, object[]>(this.LogMethodLeave);
				//if (Logger.Verbosity <= LogVerbosities.All && methodHandle != methodLeave)
				//	this.LogMethodEnter(null, null, 0, methodLeave, new object[]{ methodHandle, returnValues});

				this.xmlWriter.WriteStartElement(XmlNameMethodLeave);
				this.WriteAttributes(callerName, callerFilePath, callerLineNumber);

				if (methodInfo != null)
				{
					if (returnValues == null)
						returnValues = new object[0];
					
					this.xmlWriter.WriteStartElement(methodInfo.Name);
						this.xmlWriter.WriteElementString(XmlNameDeclaringType, methodInfo.DeclaringType.AssemblyQualifiedName);
						this.WriteMethodAttributes(methodInfo);
						this.WriteGenericTypeParameters(methodInfo);

						int i = 0;
						if (methodInfo.ReturnType != null)
						{
							this.xmlWriter.WriteStartElement(XmlNameReturnParameter);
								this.xmlWriter.WriteElementString(XmlNameType, methodInfo.ReturnType.AssemblyQualifiedName);
								if (returnValues.Length > i)
									this.xmlWriter.WriteElementObject(XmlNameValue, returnValues[i]);
							this.xmlWriter.WriteEndElement();
						}

						this.xmlWriter.WriteStartElement(XmlNameOutputParameters);
							ParameterInfo[] pis = methodInfo.GetParameters();
							foreach (ParameterInfo pi in pis)
							{
								if (!pi.ParameterType.IsByRef)
									continue;
								i++;

								this.xmlWriter.WriteStartElement(pi.Name);
									this.xmlWriter.WriteElementString(XmlNameType, pi.ParameterType.AssemblyQualifiedName);
									if (returnValues.Length > i)
										this.xmlWriter.WriteElementObject(XmlNameValue, returnValues[i]);
								this.xmlWriter.WriteEndElement();
							}
						this.xmlWriter.WriteEndElement();
					this.xmlWriter.WriteEndElement();
				}

				this.xmlWriter.WriteEndElement();
				this.xmlWriter.Flush();

				//if (Logger.Verbosity <= LogVerbosities.All && methodHandle != methodLeave)
				//	this.LogMethodLeave(null, null, 0, methodLeave, new object[0]);
			}
		}

		public override void LogExceptionThrown(string callerName, string callerFilePath, int callerLineNumber, ExceptionWrapper exception)
		{
			lock(this.xmlWriter)
			{
				//Delegate exceptionThrown = new Action<string, string, int, Exception>(this.LogExceptionThrown);
				//if (Logger.Verbosity <= LogVerbosities.All)
				//	this.LogMethodEnter(null, null, 0, exceptionThrown, new object[] { exception });

				this.xmlWriter.WriteStartElement(XmlNameException);
					this.WriteAttributes(callerName, callerFilePath, callerLineNumber);
					this.xmlWriter.WriteElementString(XmlNameType, exception.GetType().AssemblyQualifiedName);
					this.xmlWriter.WriteElementString(XmlNameExceptionMessage, exception.Message);
					this.xmlWriter.WriteElementString(XmlNameStackTrace, exception.StackTrace);
					if (exception.Data != null)
					{
						this.xmlWriter.WriteStartElement(XmlNameExceptionData);
							IDictionaryEnumerator enumerator = exception.Data.GetEnumerator();
							while(enumerator.MoveNext())
							{
								this.xmlWriter.WriteStartElement(XmlNameData);
									this.xmlWriter.WriteElementObject(XmlNameKey, enumerator.Key);
									this.xmlWriter.WriteElementObject(XmlNameValue, enumerator.Value);
								this.xmlWriter.WriteEndElement();
							}
						this.xmlWriter.WriteEndElement();
					}
					if (exception.InnerException != null)
						this.LogExceptionThrown(null, null, 0, exception.InnerException);
				this.xmlWriter.WriteEndElement();
				this.xmlWriter.Flush();

				//if (Logger.Verbosity <= LogVerbosities.All)
				//	this.LogMethodLeave(null, null, 0, exceptionThrown, new object[0]);
			}
		}
	}
}
